package backend

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/stellar/go/clients/horizonclient"
	"github.com/stellar/go/keypair"
	"github.com/stellar/go/network"
	hProtocol "github.com/stellar/go/protocols/horizon"
	"github.com/stellar/go/txnbuild"
	"github.com/stellar/kelp/gui/model2"
	"github.com/stellar/kelp/plugins"
	"github.com/stellar/kelp/support/kelpos"
	"github.com/stellar/kelp/support/networking"
	"github.com/stellar/kelp/support/toml"
	"github.com/stellar/kelp/trader"
)

const issuerSeed = "SANPCJHHXCPRN6IIZRBEQXS5M3L2LY7EYQLAVTYD56KL3V7ABO4I3ISZ"

var centralizedPricePrecisionOverride = int8(6)
var centralizedVolumePrecisionOverride = int8(1)
var centralizedMinBaseVolumeOverride = float64(30.0)
var centralizedMinQuoteVolumeOverride = float64(10.0)

type autogenerateBotRequest struct {
	UserData UserData `json:"user_data"`
}

func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) {
	bodyBytes, e := ioutil.ReadAll(r.Body)
	if e != nil {
		s.writeErrorJson(w, fmt.Sprintf("error reading request input: %s", e))
		return
	}
	log.Printf("autogenerateBot requestJson: %s\n", string(bodyBytes))

	var req autogenerateBotRequest
	e = json.Unmarshal(bodyBytes, &req)
	if e != nil {
		s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes)))
		return
	}
	userID := req.UserData.ID
	if strings.TrimSpace(userID) == "" {
		s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID"))
		return
	}

	kp, e := keypair.Random()
	if e != nil {
		s.writeError(w, fmt.Sprintf("error generating keypair: %s\n", e))
		return
	}

	// make and register bot, which places it in the initial bot state
	bot := model2.MakeAutogeneratedBot()
	e = s.kos.BotDataForUser(req.UserData.toUser()).RegisterBot(bot)
	if e != nil {
		// the bot is not registered at this stage so we don't throw a KelpError here
		s.writeError(w, fmt.Sprintf("error registering bot: %s\n", e))
		return
	}

	e = s.setupOpsDirectory(userID)
	if e != nil {
		// the bot is not registered at this stage so we don't throw a KelpError here
		s.writeError(w, fmt.Sprintf("error setting up ops directory: %s\n", e))
		return
	}

	filenamePair := bot.Filenames()
	sampleTrader := s.makeSampleTrader(kp.Seed())
	traderFilePath := s.botConfigsPathForUser(userID).Join(filenamePair.Trader)
	log.Printf("writing autogenerated bot config to file: %s\n", traderFilePath.AsString())
	e = toml.WriteFile(traderFilePath.Native(), sampleTrader)
	if e != nil {
		// the bot is not registered at this stage so we don't throw a KelpError here
		s.writeError(w, fmt.Sprintf("error writing trader toml file: %s\n", e))
		return
	}

	sampleBuysell := makeSampleBuysell()
	strategyFilePath := s.botConfigsPathForUser(userID).Join(filenamePair.Strategy)
	log.Printf("writing autogenerated strategy config to file: %s\n", strategyFilePath.AsString())
	e = toml.WriteFile(strategyFilePath.Native(), sampleBuysell)
	if e != nil {
		s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper(
			errorTypeBot,
			bot.Name,
			time.Now().UTC(),
			errorLevelWarning,
			fmt.Sprintf("could not write strategy toml file: %s\n", e),
		))
		return
	}

	// we only want to start initializing bot once it has been created, so we only advance state if everything is completed
	go func() {
		e := s.setupTestnetAccount(kp.Address(), kp.Seed(), bot.Name)
		if e != nil {
			s.addKelpErrorToMap(req.UserData, makeKelpErrorResponseWrapper(
				errorTypeBot,
				bot.Name,
				time.Now().UTC(),
				errorLevelError,
				fmt.Sprintf("error setting up account for bot '%s': %s\n", bot.Name, e),
			).KelpError)
			return
		}

		e = s.kos.BotDataForUser(req.UserData.toUser()).AdvanceBotState(bot.Name, kelpos.InitState())
		if e != nil {
			s.addKelpErrorToMap(req.UserData, makeKelpErrorResponseWrapper(
				errorTypeBot,
				bot.Name,
				time.Now().UTC(),
				errorLevelError,
				fmt.Sprintf("error advancing bot state after setting up account for bot '%s': %s\n", bot.Name, e),
			).KelpError)
			return
		}
	}()

	botJSON, e := json.Marshal(*bot)
	if e != nil {
		s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper(
			errorTypeBot,
			bot.Name,
			time.Now().UTC(),
			errorLevelWarning,
			fmt.Sprintf("unable to serialize bot: %s\n", e),
		))
		return
	}

	w.WriteHeader(http.StatusOK)
	w.Write(botJSON)
}

func (s *APIServer) setupTestnetAccount(address string, signer string, botName string) error {
	// this function runs in testnet mode only
	client := s.apiTestNet
	fundedAccount, e := s.checkFundAccount(client, address, botName)
	if e != nil {
		return fmt.Errorf("error checking and funding account: %s", e)
	}

	var txOps []txnbuild.Operation
	trustOp := txnbuild.ChangeTrust{
		Line: txnbuild.CreditAsset{
			Code:   "COUPON",
			Issuer: "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI",
		}.MustToChangeTrustAsset(),
	}
	txOps = append(txOps, &trustOp)

	paymentOp := txnbuild.Payment{
		Destination: address,
		Amount:      "1000.0",
		Asset: txnbuild.CreditAsset{
			Code:   "COUPON",
			Issuer: "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI",
		},
		SourceAccount: "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI",
	}
	txOps = append(txOps, &paymentOp)

	tx, e := txnbuild.NewTransaction(
		txnbuild.TransactionParams{
			SourceAccount: fundedAccount,
			Operations:    txOps,
			Timebounds:    txnbuild.NewInfiniteTimeout(),
			BaseFee:       100,
			// If IncrementSequenceNum is true, NewTransaction() will call `sourceAccount.IncrementSequenceNumber()`
			// to obtain the sequence number for the transaction.
			// If IncrementSequenceNum is false, NewTransaction() will call `sourceAccount.GetSequenceNumber()`
			// to obtain the sequence number for the transaction.
			// leaving as true since that's what it was in the old sdk so we want to maintain backward compatibility and we
			// need to increment the seq number on the account somewhere to use the next seq num
			IncrementSequenceNum: true,
		},
	)
	if e != nil {
		return fmt.Errorf("cannot make transaction to create trustline transaction for account %s for bot '%s': %s", address, botName, e)
	}

	for _, s := range []string{signer, issuerSeed} {
		kp, e := keypair.Parse(s)
		if e != nil {
			return fmt.Errorf("cannot parse seed  %s required for signing: %s", s, e)
		}

		tx, e = tx.Sign(network.TestNetworkPassphrase, kp.(*keypair.Full))
		if e != nil {
			return fmt.Errorf("cannot sign trustline transaction for account %s for bot '%s': %s", address, botName, e)
		}
	}

	txn64, e := tx.Base64()
	if e != nil {
		return fmt.Errorf("cannot convert trustline transaction to base64 for account %s for bot '%s': %s", address, botName, e)
	}

	resp, e := client.SubmitTransactionXDR(txn64)
	if e != nil {
		return fmt.Errorf("error submitting change trust transaction for address %s for bot '%s': %s", address, botName, e)
	}

	log.Printf("successfully added trustline for address %s for bot '%s': %v\n", address, botName, resp)
	return nil
}

func (s *APIServer) checkFundAccount(client *horizonclient.Client, address string, botName string) (*hProtocol.Account, error) {
	account, e := client.AccountDetail(horizonclient.AccountRequest{AccountID: address})
	if e == nil {
		log.Printf("account already exists %s for bot '%s', no need to fund via friendbot\n", address, botName)
		return &account, nil
	} else if e != nil {
		var herr *horizonclient.Error
		switch t := e.(type) {
		case *horizonclient.Error:
			herr = t
		case horizonclient.Error:
			herr = &t
		default:
			return nil, fmt.Errorf("unexpected error when checking for existence of account %s for bot '%s': %s", address, botName, e)
		}

		if herr.Problem.Status != 404 {
			return nil, fmt.Errorf("unexpected horizon error code when checking for existence of account %s for bot '%s': %d (%v)", address, botName, herr.Problem.Status, *herr)
		}
	}

	if !strings.Contains(client.HorizonURL, "test") {
		log.Printf("not attempting to create mainnet account %s for bot '%s' since mainnet account does not exist\n", address, botName)
	}

	// since it's a 404 we want to continue funding below
	var fundResponse interface{}
	e = networking.JSONRequest(http.DefaultClient, "GET", "https://friendbot.stellar.org/?addr="+address, "", nil, &fundResponse, "")
	if e != nil {
		return nil, fmt.Errorf("error funding address %s for bot '%s': %s", address, botName, e)
	}
	log.Printf("successfully funded account %s for bot '%s': %s\n", address, botName, fundResponse)

	// refetch account to confirm
	account, e = client.AccountDetail(horizonclient.AccountRequest{AccountID: address})
	if e != nil {
		var herr *horizonclient.Error
		switch t := e.(type) {
		case *horizonclient.Error:
			herr = t
		case horizonclient.Error:
			herr = &t
		default:
			return nil, fmt.Errorf("unexpected error when checking for existence of account %s for bot '%s': %s", address, botName, e)
		}

		return nil, fmt.Errorf("horizon error when checking for existence of account %s for bot '%s': %d (%v) -- could this be caused because horizon has not ingested this data yet? (programmer: maybe create hProtocol.Account instance manually instead of fetching)", address, botName, herr.Problem.Status, *herr)
	}
	return &account, nil
}

func (s *APIServer) makeSampleTrader(seed string) *trader.BotConfig {
	return trader.MakeBotConfig(
		"",
		seed,
		"XLM",
		"",
		"COUPON",
		"GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI",
		60,
		15000,
		5,
		"both",
		0,
		0,
		s.horizonTestnetURI,
		nil,
		"",
		"",
		&trader.FeeConfig{
			CapacityTrigger: 0.8,
			Percentile:      90,
			MaxOpFeeStroops: 5000,
		},
		&centralizedPricePrecisionOverride,
		&centralizedVolumePrecisionOverride,
		&centralizedMinBaseVolumeOverride,
		&centralizedMinQuoteVolumeOverride,
	)
}

func makeSampleBuysell() *plugins.BuySellConfig {
	return plugins.MakeBuysellConfig(
		0.001,
		0.001,
		0.0,
		0.0,
		true,
		10.0,
		"exchange",
		"kraken/XXLM/ZUSD",
		"fixed",
		"1.0",
		[]plugins.StaticLevel{
			plugins.StaticLevel{
				SPREAD: 0.0010,
				AMOUNT: 100.0,
			}, plugins.StaticLevel{
				SPREAD: 0.0015,
				AMOUNT: 100.0,
			}, plugins.StaticLevel{
				SPREAD: 0.0020,
				AMOUNT: 100.0,
			}, plugins.StaticLevel{
				SPREAD: 0.0025,
				AMOUNT: 100.0,
			},
		},
	)
}
